/*
 * "Meteoroids"
 * TCSS491 - Project 4
 * Travis Lewis
 * Steve Hipolito
 */

import com.sun.j3d.utils.universe.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

import javax.media.j3d.*;
import javax.swing.*;
import javax.vecmath.*;

public class AsteroidsMain {
	// Width of the extent in meters.
	private static final float EXTENT_WIDTH = 10; //20
	// Physics updates per second (approximate).
	private static final int UPDATE_RATE = 30; //1000
	// Number of full iterations of the collision detection and resolution system.
	private static final int COLLISION_ITERATIONS = 10;

	private static final int MIN_ASTEROIDS = 1;
	private static final int ACTIVE_BULLETS = 30; //for cheat mode
	private static final int ACTIVE_ENEMY_BULLETS = 3;
	private static final float BULLET_SPEED = 20f;
	private static final float BULLET_TIME = 800f;
	private static final float ENEMY_BULLET_SPEED = 10f;
	private static final float ENEMY_BULLET_TIME = 600f;
	private static final float SHIP_ACCEL = UPDATE_RATE/50f;
	private static final float ASTEROID_SPEED = UPDATE_RATE / 15;
	private static final float TURN_SPEED = 0.2f;
	private static final float ENEMY_SPEED = UPDATE_RATE / 5;
	private static final long INITIAL_ENEMY_DELAY = 0000;
	private static final float ENEMY_XSPAWN_DISTANCE = 
			EXTENT_WIDTH+EXTENT_WIDTH/4; //just has to be outside the extent

	private GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
	final Canvas3D canvas3D = new Canvas3D(config);
	final BranchGroup scene = new BranchGroup();

	private ArrayList<Polygon> asteroids;
	private Polygon ship;
	private Polygon enemy;
	private ArrayList<Bullet> bullets;
	private Bullet firedBullet;
	private ArrayList<Bullet> enemyBullets;
	private Bullet enemyFiredBullet;
	
	private long startUpTime;
	private boolean enemy_offscreen = false;
	
	ParticleManager particle_manager;

	private int turn_dir = 0; //positive = CCW, used to turn ship
	private boolean is_accel = false; //true when ship is moving forward
	private boolean is_fired = false; //true when ship has fired a bullet
	
	private boolean shipDead = false;
	private int level = 0;
	private int score = 0;
	private int lives = 5;
	private boolean game_over = false;
	private boolean cheatMode = false;
	
	private JCheckBox cheatCB = new JCheckBox("Cheat");
	private JLabel level_label = new JCLabel("");
	private JLabel score_label = new JCLabel("");
	private JLabel lives_label = new JCLabel("");
	
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				new AsteroidsMain().createAndShowGUI();
			}
		});
	}

	public AsteroidsMain() {
		ship = new Polygon(2, 3, 1f, 0, 0, 0, 0, ((float)Math.PI)/2f, 0, EXTENT_WIDTH, 0.04f, new Color3f(1,1,1));
		bullets = new ArrayList<Bullet>();
		enemyBullets = new ArrayList<Bullet>();
		asteroids = new ArrayList<Polygon>();
		nextLevel();
		
		enemy = new Polygon(3, 6, 1f, -ENEMY_XSPAWN_DISTANCE,
				(float) (Math.random()*(2*EXTENT_WIDTH)-EXTENT_WIDTH), ENEMY_SPEED, 0, 
				-((float)Math.PI)/3f, 0, EXTENT_WIDTH, 0.05f, new Color3f(0.2f,0.2f,1));
		
		startUpTime = System.currentTimeMillis();
		particle_manager = new ParticleManager(scene, UPDATE_RATE);
	}

	private void createAndShowGUI() {
		// Fix for background flickering on some platforms
		System.setProperty("sun.awt.noerasebackground", "true");

		SimpleUniverse simpleU = new SimpleUniverse(canvas3D);
		simpleU.getViewingPlatform().setNominalViewingTransform();
		//simpleU.getViewer().getView().setSceneAntialiasingEnable(true);

		// Add a scaling transform that resizes the virtual world to fit
		// within the standard view frustum.
		BranchGroup trueScene = new BranchGroup();
		TransformGroup worldScaleTG = new TransformGroup();
		Transform3D t3D = new Transform3D();
		t3D.setScale(.9 / EXTENT_WIDTH);
		worldScaleTG.setTransform(t3D);
		trueScene.addChild(worldScaleTG);
		scene.setCapability(Group.ALLOW_CHILDREN_WRITE);
		scene.setCapability(Group.ALLOW_CHILDREN_EXTEND);
		worldScaleTG.addChild(scene);

		scene.addChild(ship.BG); //add ship
		scene.addChild(enemy.BG); //add enemy
		//add asteroids

		final TransformGroup extentTransform = new TransformGroup();
		extentTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		//extentTransform.addChild(createExtent());
		scene.addChild(extentTransform);
		simpleU.addBranchGraph(trueScene);
		
		String instructions = "Controls:\nW/UP arrow key - thrust forward\n" +
				"A/LEFT arrow key - rotate CCW\nS/RIGHT arrow key - rotate CW" +
				"\nSPACE - fire bullets\nESC - exit game\n\n" +
				"Created By:\nTravis Lewis\nSteve Hipolito";
		
		JOptionPane.showMessageDialog(null, instructions,"Welcome to Meteoroids!",JOptionPane.PLAIN_MESSAGE);

		final JFrame appFrame = new JFrame();//JFrame("Meteoroids!");
		appFrame.setTitle("Meteoroids!");
		appFrame.setLayout(new BorderLayout());

		appFrame.setResizable(false);
		appFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		appFrame.setPreferredSize(new Dimension(650,650));

		appFrame.add(canvas3D,BorderLayout.CENTER);
		
		cheatCB.setMnemonic(cheatCB.getText().charAt(0));
		cheatCB.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				cheatMode = !cheatMode;
				canvas3D.requestFocus();
			}
		});
		
		JPanel hud = new JPanel(new GridLayout(1,4));
		hud.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createRaisedBevelBorder(),
				BorderFactory.createLoweredBevelBorder()));
		hud.add(level_label);
		hud.add(lives_label);
		hud.add(score_label);
		hud.add(cheatCB);
		appFrame.add(hud,BorderLayout.NORTH);
		
		appFrame.pack();
		canvas3D.requestFocus();
		//if (Toolkit.getDefaultToolkit().isFrameStateSupported(Frame.MAXIMIZED_BOTH))
		//	appFrame.setExtendedState(appFrame.getExtendedState() | Frame.MAXIMIZED_BOTH);

		canvas3D.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent arg0) {
				char c = arg0.getKeyChar();

				if( c == 'a' || c == 'A' || arg0.getKeyCode() == KeyEvent.VK_LEFT)
					turn_dir = 1;
				if( c == 'd' || c == 'D' || arg0.getKeyCode() == KeyEvent.VK_RIGHT)
					turn_dir = -1;
				if(c == 'w' || c == 'W' || arg0.getKeyCode() == KeyEvent.VK_UP)
					is_accel = true;
				if(cheatMode && (c == 's' || c == 'S' || arg0.getKeyCode() == KeyEvent.VK_DOWN))
					ship.velocity.x = ship.velocity.y = ship.angularVelocity = 0;
				if(arg0.getKeyCode() == KeyEvent.VK_ESCAPE) {
					//press ESC to exit the game
					appFrame.removeAll();
					appFrame.setEnabled(false);
					appFrame.dispose();
					System.exit(0);
				}
				if(arg0.getKeyCode() == 32 && cheatMode)
					is_fired = true;
			}

			public void keyReleased(KeyEvent arg0) {
				char c = arg0.getKeyChar();

				if( c == 'a' || c == 'A' || arg0.getKeyCode() == KeyEvent.VK_LEFT 
						|| c == 'd' || c == 'D' || arg0.getKeyCode() == KeyEvent.VK_RIGHT)
					turn_dir = 0;
				if(c == 'w' || c == 'W' || arg0.getKeyCode() == KeyEvent.VK_UP)
					is_accel = false;	
				if(arg0.getKeyCode() == 32)
					is_fired = !cheatMode;
			}

			public void keyTyped(KeyEvent arg0) {
				char c = arg0.getKeyChar();
				
				if (c == 'b' && shipDead) {
					ship = new Polygon(2, 3, 1f, 0, 0, 0, 0, ((float)Math.PI)/2f, 0, EXTENT_WIDTH, 0.04f, new Color3f(1,1,1));
					scene.addChild(ship.BG);
					shipDead = false;
				}
				if (c == 'c')
					cheatCB.doClick();
			}
		});	

		new Timer(1000 / UPDATE_RATE, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				canvas3D.stopRenderer();
				if(!game_over)
					tick();
				else {
					String msg = "Game Over\nFinal Score: " + String.valueOf(score);
					JOptionPane.showMessageDialog(null, msg,"Game Over",JOptionPane.PLAIN_MESSAGE);
					appFrame.removeAll();
					appFrame.setEnabled(false);
					appFrame.dispose();
					System.exit(0);
				}
				canvas3D.startRenderer();
			}
		}).start();

		appFrame.setVisible(true);
	}

	private void tick() {
		//update ship
		ship.updateState(1f / UPDATE_RATE);

		ship.orientation += turn_dir * TURN_SPEED; //rotate ship
		if(is_accel && !shipDead) {
			Vector2f dir = ship.getForward();
			ship.velocity.x += dir.x * SHIP_ACCEL;
			ship.velocity.y += dir.y * SHIP_ACCEL;
			ship.angularVelocity = 0;
			particle_manager.addThrusterParticles(ship);
			
			//dampen speed, hopefully to give a max speed
			Vector2f damp = new Vector2f(ship.velocity.x, ship.velocity.y);
			damp.scale(-0.01f);
			ship.velocity.add(damp);
		}
		else {
			//else we should dampen faster
			if(ship.velocity.length() > 0.1f) {
				Vector2f damp = new Vector2f(ship.velocity.x, ship.velocity.y);
				damp.scale(-0.05f);
				ship.velocity.add(damp);
			}
			else 
				ship.velocity = new Vector2f();
		}
		
		if(Math.abs(ship.position.x) > 1.175f * EXTENT_WIDTH) { 
			float side = (ship.position.x > 0) ? -1f : 1f ;
			ship.position.x = 1.1749f * EXTENT_WIDTH * side;
		}
		if(Math.abs(ship.position.y) > 1.1f * EXTENT_WIDTH) {
			float side = (ship.position.y > 0) ? -1f : 1f ;
			ship.position.y = 1.09f * EXTENT_WIDTH * side;
		}

		ship.updateTransformGroup();

		//update asteroid state
		for(Polygon p : asteroids) 
			p.updateState(1f / UPDATE_RATE);

		//update bullets
		if (!shipDead && is_fired && bullets.size() < ACTIVE_BULLETS) {
			if (!cheatMode) is_fired = false;
			Vector2f dir = ship.getForward();
			firedBullet = new Bullet(1, ship.position.x, ship.position.y, 
					BULLET_SPEED*dir.x + ship.velocity.x, BULLET_SPEED*dir.y + ship.velocity.y, 
					0, 0, EXTENT_WIDTH * .005f, new Color3f(1, 1, 1), new Color3f(1, 1, 1));
			firedBullet.timerStart = System.currentTimeMillis();
			bullets.add(firedBullet);
			scene.addChild(firedBullet.BG);
		}
		if (!bullets.isEmpty()) {
			int off = 0;
			for(int i = 0 ; i < bullets.size() ; i++) {
				bullets.get(i - off).updateState(1f / UPDATE_RATE);
				if(Math.abs(bullets.get(i - off).position.x) > EXTENT_WIDTH * 1.1f) {
					float side = (bullets.get(i - off).position.x > 0 ) ? -1f : 1f ;
					bullets.get(i - off).position.x = EXTENT_WIDTH * 1.09f;
					bullets.get(i - off).position.x *= side;
				}
				if(Math.abs(bullets.get(i - off).position.y) > EXTENT_WIDTH * 1.1f) {
					float side = (bullets.get(i - off).position.y > 0 ) ? -1f : 1f ;
					bullets.get(i - off).position.y = EXTENT_WIDTH * 1.09f;
					bullets.get(i - off).position.y *= side;
				}
				
				bullets.get(i - off).updateTransformGroup();
				
				// bullet-enemy collisions
				CollisionInfo ci = CollisionHandler.checkAndResolveCollision(enemy, bullets.get(i - off));
				if(null != ci) {
					particle_manager.addEnemyDestructionParticles(enemy);
					repositionEnemy();
					scene.removeChild(bullets.get(i - off).BG);
					bullets.remove(i - off);
					off++;
					score += 25;
				}
			}
			
			if(!bullets.isEmpty()) {
				long elapsedTime = System.currentTimeMillis() - bullets.get(0).timerStart;
				if (elapsedTime > BULLET_TIME) {
					scene.removeChild(bullets.get(0).BG);
					bullets.remove(0);
				}
			}
		}
		
		//update enemy bullets
		if (enemyBullets.size() < ACTIVE_ENEMY_BULLETS) {
			Vector2f dir = new Vector2f((float)(Math.random()*2)-1, (float)(Math.random()*2)-1);
			dir.normalize();
			enemyFiredBullet = new Bullet(1, enemy.position.x, enemy.position.y, 
					ENEMY_BULLET_SPEED*dir.x + enemy.velocity.x, ENEMY_BULLET_SPEED*dir.y + enemy.velocity.y, 
					0, 0, EXTENT_WIDTH * .005f, new Color3f(1, 1, 1), new Color3f(1, 1, 1));
			enemyFiredBullet.timerStart = System.currentTimeMillis();
			enemyBullets.add(enemyFiredBullet);
			scene.addChild(enemyFiredBullet.BG);
		}
		if (!enemyBullets.isEmpty()) {
			int off = 0;
			for(int i = 0 ; i < enemyBullets.size() ; i++) {
				enemyBullets.get(i - off).updateState(1f / UPDATE_RATE);
				enemyBullets.get(i - off).updateTransformGroup();
				
				// bullet-ship collisions
				CollisionInfo ci = CollisionHandler.checkAndResolveCollision(ship, enemyBullets.get(i - off));
				if(null != ci && !shipDead) {
					scene.removeChild(enemyBullets.get(i - off).BG);
					shipHit();
					enemyBullets.remove(i - off);
					off++;
				}
			}
			
			if(!enemyBullets.isEmpty()) {
				long elapsedTime = System.currentTimeMillis() - enemyBullets.get(0).timerStart;
				if (elapsedTime > ENEMY_BULLET_TIME) {
					scene.removeChild(enemyBullets.get(0).BG);
					enemyBullets.remove(0);
				}
			}
		}
		
		//update enemy
		updateEnemy();

		//check collisions: 
		checkCollisions(bullets, true);
		checkCollisions(enemyBullets, false);
		handleAsteroidAsteroidCollisions();
		
		//update asteroid locations
		for(Polygon p : asteroids) {
			p.updateTransformGroup();
			
			if(Math.abs(p.position.x) - p.width > 1.1f * EXTENT_WIDTH) {
				float side = (p.position.x > 0) ? -1f : 1f;
				p.position.x = p.width * side + 1.09f * EXTENT_WIDTH * side;
				//p.position.x = -p.position.x;
			}
			if(Math.abs(p.position.y) - p.width > 1.1f * EXTENT_WIDTH) {
				float side = (p.position.y > 0) ? -1f : 1f;
				p.position.y = p.width * side + 1.09f * EXTENT_WIDTH * side;
				//p.position.y = -p.position.y;
			}
		}
		
		//update particles
		particle_manager.updateParticles();
		
		//if there's no asteroids left, move on to the next level
		if(asteroids.isEmpty())
			nextLevel();
		
		updateHUD();
	}
	
	private boolean handleAsteroidAsteroidCollisions() {
		boolean collided = false;
		for(int i = 0 ; i < COLLISION_ITERATIONS; i++) {
			for(Polygon p : asteroids) {
				for(Polygon p2 : asteroids) {
					CollisionInfo ci = CollisionHandler.checkAndResolveCollision(p, p2);
					if(null != ci) {
						collided = true;
						//if(allow_asteroid_shrapnel) 
						//	particle_manager.addAsteroidAsteroidParticles(p, p2, ci);
					}
				}
			}
		}
		return collided;
	}
	
	private boolean checkCollisions(ArrayList<Bullet> the_bullets, boolean can_score) {
		boolean collision = false;
		
		int asize = asteroids.size();
		int bsize = the_bullets.size();
		boolean[] asteroid_remove = new boolean[asteroids.size()];
		boolean[] bullet_remove = new boolean[the_bullets.size()];

		//for each asteroid
		for(int p = 0 ; p < asize ; p++) {

			//asteroid-bullet collisions
			for(int b = 0 ; b < bsize ; b++) {
				CollisionInfo ci = CollisionHandler.checkAndResolveCollision(asteroids.get(p), the_bullets.get(b));
				if(null != ci) {
					if(can_score)
						score += asteroids.get(p).getGeneration() * 5;
					
					breakAsteroid(asteroids.get(p), the_bullets.get(b));
					particle_manager.addBulletAsteroidParticles(asteroids.get(p), the_bullets.get(b));
					particle_manager.addAsteroidDestructionParticles(asteroids.get(p));
					asteroid_remove[p] = true;
					bullet_remove[b] = true;
				}
			}

			//asteroid-ship collisions
			CollisionInfo ci = CollisionHandler.checkAndResolveCollision(ship, asteroids.get(p));
			if (ci != null && !shipDead)
				shipHit();

			//asteroid-enemy collisions
			CollisionHandler.checkAndResolveCollision(enemy, asteroids.get(p));				
		}
		
		//ship-enemy collisions
		CollisionHandler.checkAndResolveCollision(ship, enemy);

		int disp = 0;
		for(int k = 0 ; k < asteroid_remove.length ; k++) {
			if(true == asteroid_remove[k]) {
				scene.removeChild(asteroids.get(k - disp).BG);
				asteroids.remove(k - disp);
				disp++;
			}
		}
		disp = 0;
		for(int k = 0 ; k < bullet_remove.length ; k++) {
			if(true == bullet_remove[k]) {
				scene.removeChild(the_bullets.get(k - disp).BG);
				the_bullets.remove(k - disp);
				disp++;
			}
		}
		
		return collision;
	}

	private void breakAsteroid(Polygon asteroid, Bullet bullet) {
		if(asteroid.getGeneration() < Polygon.MAX_GENERATIONS) {
			for(int i = 0 ; i < 2 ; i++) {
				int generation = asteroid.getGeneration()+1;
				int sides = Polygon.MAX_SIDES - generation;//asteroid.getGeneration();
				float mass = asteroid.mass/2;
				float px = asteroid.position.x;
				float py = asteroid.position.y;
				float vx = asteroid.velocity.x + (bullet.velocity.x * bullet.mass)/asteroid.mass;
				float vy = asteroid.velocity.y + (bullet.velocity.y * bullet.mass)/asteroid.mass;
				float or = (float)(Math.PI * Math.random());
				float av = asteroid.angularVelocity;
				float ra = 0.2f / (float)generation;
				
				Polygon p = new Polygon(1, sides, mass, px, py, vx, vy, or, av,
						EXTENT_WIDTH, ra, asteroid.color , generation);

				asteroids.add(p);
				scene.addChild(p.BG);
			}
		}
	}
	
	private void updateEnemy() {
		long gameDuration = System.currentTimeMillis() - startUpTime;
		if (gameDuration > INITIAL_ENEMY_DELAY) {
			if (Math.abs(enemy.position.x) > ENEMY_XSPAWN_DISTANCE ||
					Math.abs(enemy.position.y) > ENEMY_XSPAWN_DISTANCE)
			{
				if(!enemy_offscreen) {
					repositionEnemy();
					enemy_offscreen = true;
				}
			}
			else
				enemy_offscreen = false;
			
			enemy.updateState(1f / UPDATE_RATE);
			enemy.updateTransformGroup();
		}
	}
	
	private void repositionEnemy() {
		float side = (enemy.position.x > 0) ? 1f : -1f;
		
		enemy.position.x = ( 2f + (5f * (float)Math.random())) 
				* ENEMY_XSPAWN_DISTANCE;
		enemy.position.x *= side;
		enemy.position.y = (float) (Math.random() 
				* (2*EXTENT_WIDTH)-EXTENT_WIDTH);
		
		enemy.velocity.y = 0;
		enemy.velocity.x = ENEMY_SPEED * (-side);
		
		enemy.orientation = -((float)Math.PI)/3f;//0;
		enemy.angularVelocity = 0;
	}
	
	private void shipHit() {
		if (!cheatMode) {
        		particle_manager.addShipDestructionParticles(ship);
        
        		lives--;
        		if(lives < 0) { // i hate when games don't give you a 0th life...
        			lives = 0;
        			game_over = true;
        		}
        		
        		Vector2f pos;
        		boolean valid_pos;
        		do{
        			valid_pos = true;
        			pos = getRandomLocation(EXTENT_WIDTH);
        			for(Polygon a : asteroids) {
        				if(getDistance(pos, a.position) < 3f)
        					valid_pos = false;
        			}
        		} while(!valid_pos);
        		
        		ship.position = pos;
        		ship.orientation = (float)(Math.PI / 2);
        		ship.velocity = new Vector2f();
        		ship.angularVelocity = 0;
        		
        		repositionEnemy();
		}
	}
	
	// spawns a new set of asteroids
	private void nextLevel() {
		ship.position = new Vector2f();
		ship.velocity = new Vector2f();
		ship.orientation = (float)(Math.PI / 2);
		ship.angularVelocity = 0;
		
		level++;
		for(int i = 0 ; i < MIN_ASTEROIDS + level ; i++) {
			Vector2f pos = getRandomLocation(EXTENT_WIDTH);
			do {
				pos = getRandomLocation(EXTENT_WIDTH);
			} while(getDistance(pos, ship.position) < 4f);
			
			Polygon p = Polygon.getRandomAsteroid(EXTENT_WIDTH, ASTEROID_SPEED, 
					pos.x, pos.y);
			asteroids.add(p);
			scene.addChild(p.BG);
		}
	}
	
	private void updateHUD() {
		level_label.setText("  Level: " + level);
		lives_label.setText("Lives: " + lives);
		score_label.setText("Score: " + score);
	}
	
	private Vector2f getRandomLocation(float width) {
		Vector2f v = new Vector2f();
		v.x = (float)(Math.random() - 0.5) * 2f * width;
		v.y = (float)(Math.random() - 0.5) * 2f * width;
		return v;
	}
	
	private float getDistance(Vector2f a, Vector2f b) {
		Vector2f dist = new Vector2f(a.x - b.x, a.y - b.y);
		return dist.length();
	}
	
	@SuppressWarnings("serial")
	public class JCLabel extends JLabel {
		public JCLabel(String text) {
			setText(text);
			setFont(new Font("Arial Bold", Font.BOLD, 15));
		}
	}
}
